/**
 * Team members:
 * HengXing Liu 2022229044
 * YiYang Zhang 2022229047
 * ZeJun Liu 2022229092
 */
package project;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

import static java.math.BigDecimal.ROUND_HALF_UP;

/**
 * This class is used to calculate mathematical expressions
 */

public class MathematicalEvaluator {

    private BigDecimal result;
    private Tokenizer tokenizer;

    public MathematicalEvaluator() {
        result = new BigDecimal(0);
    }

    public void setTokenizer(Tokenizer tokenizer) {
        this.tokenizer = tokenizer;
    }

    public BigDecimal Evaluator(VariableManage vm, PrecisionManage pm,FunctionManage fm) throws TokenException, DivisionErrorException, LexicalErrorException, VariableException, SyntaxErrorException {
        Stack<Double> num = new Stack<>();
        Stack<FunOp> opr = new Stack<>();

        while (tokenizer.hasNextToken()) {
            Token t = tokenizer.readNextToken();
            if (t.isNumber()) {
                num.push(t.getNumber());
//                    System.out.println(t.getNumber()+"push");
            }
            else if (t.isIdentifier()) {
                if(vm.getAllValue().containsKey(t.getIdentifier())){
                    num.push(vm.getValue(t.getIdentifier()));
                }
                else if(fm.isContainFName(t.getIdentifier())){
                    FunOp funop = new FunOp(t.getIdentifier(), fm.getFArity(t.getIdentifier()));
                    opr.push(funop);
                }
                else if(t.getIdentifier().equals("sin") || t.getIdentifier().equals("cos") || t.getIdentifier().equals("tan") || t.getIdentifier().equals("abs")){
                    FunOp funop = new FunOp(t.getIdentifier());
                    opr.push(funop);
                }
                else{
                    throw new VariableException(t.getIdentifier()+" is not a varizble");
                }
//                    System.out.println(t.getIdentifier() + "push");
            }
            else if (t.isOperator() || t.toString().equals(",")) {
                if (!t.toString().equals(",")&&t.getOperator().equals("(")) {
                    opr.push(new FunOp("("));
//                        System.out.println("( push");
                }
                else if (!t.toString().equals(",")&&t.getOperator().equals(")")) {
                    while (!opr.empty() && !opr.peek().getOpr().equals("(")) {
                        FunOp top = opr.pop();
                        double num2 = 0.0;

                        if(num.empty()) throw new SyntaxErrorException();
                        double secondNum = num.pop();
//                            System.out.println("secondnum is " +secondNum);
                        if(top.isfree()){
                            ArrayList<Double> opNums = new ArrayList<>();
                            int numCnt = top.getArity();
                            opNums.add(secondNum);
                            while(--numCnt != 0){
                                if(num.empty()) throw new SyntaxErrorException();
                                opNums.add(num.pop());
                            }
                            Collections.reverse(opNums);
                            num2 = fm.evaluator(top.getOpr(),opNums);
                        }else{
                            if (top.getArity() == 1) {
                                num2 = calculateOneNum(top,secondNum);
                            } else if (top.getArity() == 2){
                                if(num.empty()) throw new SyntaxErrorException();
                                double firstNum = num.pop();
                                num2 = calculateTwoNum(top, firstNum, secondNum);
                            }
                        }
                        num.push(num2);
//                            System.out.println("num2 is " + num2);
                    }
                    if(opr.empty()) throw new SyntaxErrorException();
                    FunOp top = opr.pop();
                    if(!opr.empty()){
                        String nxtPeekOpr = opr.peek().getOpr();
                        if(fm.isContainFName(nxtPeekOpr)){
                            top = opr.pop();
                            ArrayList<Double> opNums = new ArrayList<>();
                            int numCnt = top.getArity();
                            while(numCnt-- != 0){
                                if(num.empty()) throw new SyntaxErrorException();
                                opNums.add(num.pop());
                            }
                            Collections.reverse(opNums);
                            double tmpRes = fm.evaluator(top.getOpr(),opNums);
                            num.push(tmpRes);
                        }
                    }

                }
                else{
                    FunOp operator;
                    if(t.toString().equals(","))operator = new FunOp(",");
                    else operator = new FunOp(t.getOperator());
                    while (!opr.empty() && (opr.peek().getPriority() <= operator.getPriority())) {
//                        FunOp operatorTop = opr.peek();
//                        System.out.println(operatorTop + " " + opr.peek().getPriority());
                        Double num2 = 0.0;
                        if(num.empty()) throw new SyntaxErrorException();
                        Double secondNum = num.pop();
                        if(opr.empty()) throw new SyntaxErrorException();
                        FunOp top = opr.pop();
                          //  System.out.println(top + "opertor pop");
                        if(top.isfree()){
                            ArrayList<Double> opNums = new ArrayList<>();
                            int numCnt = top.getArity();
                            opNums.add(secondNum);
                            while(--numCnt != 0){
                                if(num.empty()) throw new SyntaxErrorException();
                                opNums.add(num.pop());
                            }
                            Collections.reverse(opNums);
                            num2 = fm.evaluator(top.getOpr(),opNums);
                        }else{
                            if (top.getArity() == 1) {
                                num2 = calculateOneNum(top,secondNum);
                            } else if (top.getArity() == 2){
                                if(num.empty()) throw new SyntaxErrorException();
                                Double firstNum = num.pop();
                                num2 = calculateTwoNum(top, firstNum, secondNum);
                            }
                        }
                        num.push(num2);
                    }
                    if(!t.toString().equals(","))
                        opr.push(new FunOp(t.getOperator()));
                        //System.out.println(t.getOperator()+"operator push");
                }

            }
        }
        //no token
        while (!opr.empty()) {
//                System.out.println("final cal");
            Double num2 = 0.0;
            if(num.empty()) throw new SyntaxErrorException();
            Double secondNum = num.pop();
            if(opr.empty()) throw new SyntaxErrorException();
            FunOp top = opr.pop();
//                System.out.println(top+"opertor pop");
//                System.out.println((secondNum+"pop"));
            if(top.isfree()){
                ArrayList<Double> opNums = new ArrayList<>();
                int numCnt = top.getArity();
                opNums.add(secondNum);
                while(--numCnt != 0){
                    if(num.empty()) throw new SyntaxErrorException();
                    opNums.add(num.pop());
                }
                Collections.reverse(opNums);
                num2 = fm.evaluator(top.getOpr(),opNums);
            }else{
                if (top.getArity() == 1) {
                    num2 = calculateOneNum(top,secondNum);
                } else if (top.getArity() == 2){
                    if(num.empty()) throw new SyntaxErrorException();
                    Double firstNum = num.pop();
                    num2 = calculateTwoNum(top, firstNum, secondNum);
                } else {
                    throw new SyntaxErrorException();
                }
            }
            num.push(num2);
        }

        if(num.size() != 1) throw new SyntaxErrorException();

        result = BigDecimal.valueOf(num.pop());
        int pr = pm.getCurrentPrecision();
        result = result.setScale(pr, RoundingMode.HALF_UP);
        result = result.stripTrailingZeros();



        return result;
    }

    private Double calculateOneNum(FunOp top, double num){
        double num2 =0.0;
        switch ((top.getOpr())) {
            case "-U":
                num2 = -num;
                break;
            case "sin":
                num2 = Math.sin(num);
                break;
            case "cos":
                num2 = Math.cos(num);
                break;
            case "tan":
                num2 = Math.tan(num);
                break;
            case "abs":
                num2 = Math.abs(num);
                break;
        }
        return num2;

    }
    private Double calculateTwoNum(FunOp top, double firstNum, double secondNum) throws DivisionErrorException {
        Double ret = 0.0;
        if (top.getOpr().equals("+")) {
            ret = secondNum + firstNum;
        }
        if (top.getOpr().equals("-")) {
            ret = firstNum - secondNum;
        }
        if (top.getOpr().equals("*")) {
            ret = firstNum * secondNum;
        }
        if (top.getOpr().equals("/")) {
            if (secondNum == 0) throw new DivisionErrorException();
            ret = firstNum / secondNum;
        }
        if (top.getOpr().equals("^")) {
            ret = Math.pow(firstNum, secondNum);
        }
        return ret;
    }
}
